/* * Copyright (C) 2013 Simon Vig Therkildsen * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.simonvt.cathode.widget; import android.annotation.TargetApi; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.ColorMatrix; import android.graphics.ColorMatrixColorFilter; import android.graphics.Paint; import android.graphics.drawable.Drawable; import android.os.Build; import android.os.Parcel; import android.os.Parcelable; import android.os.SystemClock; import android.text.TextUtils; import android.util.AttributeSet; import android.view.ViewOutlineProvider; import com.squareup.picasso.Picasso; import com.squareup.picasso.RequestCreator; import com.squareup.picasso.Target; import com.squareup.picasso.Transformation; import java.util.ArrayList; import java.util.List; import javax.inject.Inject; import net.simonvt.cathode.CathodeApp; import net.simonvt.cathode.R; import net.simonvt.cathode.widget.animation.MaterialTransition; import timber.log.Timber; /** * Simple View used to display an image from a remote source. An URL to an image is passed to * {@link #setImage(String)}, and the view then takes care of loading and displaying it. * <p/> * The view must either have a fixed width or a fixed width. Both can also be set. If only one * dimension is fixed, * {@link #setAspectRatio(float)} must be called. The non-fixed dimension will then be calculated * by * multiplying the * fixed dimension with this value. */ public class RemoteImageView extends AspectRatioView implements Target { private static final float ANIMATION_DURATION = 1000.0f; @Inject Picasso picasso; private Drawable placeHolder; private Bitmap image; private boolean animating; private long startTimeMillis; private float fraction; private Paint paint = new Paint(); private ColorMatrix colorMatrix; private ColorMatrixColorFilter colorMatrixColorFilter; private String imageUrl; private int imageResource; private List<Transformation> transformations = new ArrayList<>(); private int resizeInsetX; private int resizeInsetY; public RemoteImageView(Context context) { this(context, null); } public RemoteImageView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public RemoteImageView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); if (!isInEditMode()) { CathodeApp.inject(context, this); } TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RemoteImageView); placeHolder = a.getDrawable(R.styleable.RemoteImageView_placeholder); if (placeHolder == null) { placeHolder = getResources().getDrawable(R.drawable.placeholder); } a.recycle(); setupOutlineProvider(); colorMatrix = new ColorMatrix(); colorMatrixColorFilter = new ColorMatrixColorFilter(colorMatrix); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) private void setupOutlineProvider() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { setOutlineProvider(ViewOutlineProvider.PADDED_BOUNDS); } } public void addTransformation(Transformation transformation) { transformations.add(transformation); } public void clearTransformations() { transformations.clear(); } public void removeTransformation(Transformation transformation) { transformations.remove(transformation); } public void setResizeInsets(int x, int y) { resizeInsetX = x; resizeInsetY = y; } public void setImage(String imageUrl) { setImage(imageUrl, false); } public void setImage(String imageUrl, boolean animateIfDifferent) { picasso.cancelRequest(this); boolean animate = animateIfDifferent && !TextUtils.equals(imageUrl, this.imageUrl); this.imageUrl = imageUrl; this.imageResource = 0; image = null; fraction = 0.0f; startTimeMillis = 0; animating = false; if (getWidth() - getPaddingLeft() - getPaddingRight() > 0 && getHeight() - getPaddingTop() - getPaddingBottom() > 0) { loadBitmap(animate); } invalidate(); } public void setImage(int imageResource) { picasso.cancelRequest(this); this.imageUrl = null; this.imageResource = imageResource; image = null; fraction = 0.0f; startTimeMillis = 0; animating = false; if (getWidth() - getPaddingLeft() - getPaddingRight() > 0 && getHeight() - getPaddingTop() - getPaddingBottom() > 0) { loadBitmap(false); } invalidate(); } private void loadBitmap(boolean animate) { fraction = 0.0f; image = null; final int width = getWidth() - getPaddingLeft() - getPaddingRight(); final int height = getHeight() - getPaddingTop() - getPaddingBottom(); RequestCreator creator = null; if (imageUrl != null) { creator = picasso.load(imageUrl); } else if (imageResource > 0) { creator = picasso.load(imageResource); } if (creator != null) { creator.resize(width - resizeInsetX, height - resizeInsetY).centerCrop(); for (Transformation transformation : transformations) { creator.transform(transformation); } creator.into(this); } if (!animate && image != null) { animating = false; startTimeMillis = 0; fraction = 1.0f; } } @Override public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom loadedFrom) { image = bitmap; animating = true; startTimeMillis = 0; fraction = 0.0f; invalidate(); } @Override public void onBitmapFailed(Drawable drawable) { Timber.d("[onBitmapFailed] %s", imageUrl); } @Override public void onPrepareLoad(Drawable drawable) { } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); if ((imageUrl != null || imageResource > 0) && w - getPaddingLeft() - getPaddingRight() > 0 && h - getPaddingTop() - getPaddingBottom() > 0) { loadBitmap(false); } } public float getFraction() { return fraction; } @Override protected void onDraw(Canvas canvas) { if (image == null) { drawPlaceholder(canvas, placeHolder, 255); return; } boolean done = true; int alpha = 0; if (animating) { if (startTimeMillis == 0) { startTimeMillis = SystemClock.uptimeMillis(); done = false; fraction = 0.0f; } else { float normalized = (SystemClock.uptimeMillis() - startTimeMillis) / ANIMATION_DURATION; done = normalized >= 1.0f; fraction = Math.min(normalized, 1.0f); alpha = (int) (0xFF * fraction); animating = alpha != 0xFF; } } if (done) { drawBitmap(canvas, image, !done, fraction); } else { drawPlaceholder(canvas, placeHolder, 0xFF - alpha); if (alpha > 0) { drawBitmap(canvas, image, !done, fraction); } invalidate(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { invalidateOutline(); } } } protected void drawPlaceholder(Canvas canvas, Drawable placeholder, int alpha) { final int width = getWidth(); final int height = getHeight(); placeHolder.setBounds(getPaddingLeft(), getPaddingTop(), width - getPaddingRight(), height - getPaddingBottom()); placeholder.setAlpha(alpha); placeholder.setAlpha(alpha); placeholder.draw(canvas); } protected void drawBitmap(Canvas canvas, Bitmap bitmap, boolean animating, float fraction) { if (animating) { colorMatrix.reset(); MaterialTransition.apply(colorMatrix, fraction); colorMatrixColorFilter = new ColorMatrixColorFilter(colorMatrix); paint.setColorFilter(colorMatrixColorFilter); } else if (paint.getColorFilter() != null) { paint.setColorFilter(null); } canvas.drawBitmap(bitmap, getPaddingLeft(), getPaddingTop(), paint); } @Override protected Parcelable onSaveInstanceState() { Parcelable superState = super.onSaveInstanceState(); SavedState state = new SavedState(superState); state.imageUrl = imageUrl; state.imageResource = imageResource; return state; } @Override protected void onRestoreInstanceState(Parcelable state) { SavedState savedState = (SavedState) state; super.onRestoreInstanceState(savedState.getSuperState()); setImage(savedState.imageUrl); } static class SavedState extends BaseSavedState { String imageUrl; int imageResource; public SavedState(Parcelable superState) { super(superState); } public SavedState(Parcel in) { super(in); imageUrl = in.readString(); imageResource = in.readInt(); } @Override public void writeToParcel(Parcel dest, int flags) { super.writeToParcel(dest, flags); dest.writeString(imageUrl); dest.writeInt(imageResource); } @SuppressWarnings("UnusedDeclaration") public static final Creator<SavedState> CREATOR = new Creator<SavedState>() { @Override public SavedState createFromParcel(Parcel in) { return new SavedState(in); } @Override public SavedState[] newArray(int size) { return new SavedState[size]; } }; } }